package com.googlecode.cswish.struts.conversion;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

import org.apache.commons.lang.ClassUtils;
import org.apache.log4j.Logger;
import org.apache.struts2.ServletActionContext;

import com.googlecode.cswish.model.PageElement;
import com.googlecode.cswish.model.PageElements;
import com.googlecode.cswish.model.PageInfo;
import com.googlecode.cswish.model.PropertyInfo;
import com.googlecode.cswish.security.Context;
import com.googlecode.cswish.security.SafeManager;
import com.googlecode.cswish.security.auth.UserInfo;
import com.googlecode.cswish.struts.Invoker;
import com.googlecode.cswish.struts.Routing;
import com.googlecode.cswish.struts.hibernate.DynamicModel;
import com.googlecode.cswish.struts.spring.BeanIntrospector;
import com.googlecode.cswish.util.ClassUtil;
import com.googlecode.cswish.util.FrameConstant;
import com.googlecode.cswish.util.Functions;
import com.googlecode.cswish.util.OgnlExUtil;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.util.ValueStack;

/**
 * Convert class to PageElement
 * 
 * convert different converter according to the class type and parent class type 
 * 
 * @author feng Date: 2009-5-5
 */
public class AutoPageConverterManager {

	private static final Logger logger = Logger.getLogger(AutoPageConverterManager.class);
	
	private Map<String, ElementConverter> classConverters = new HashMap<String, ElementConverter>();
	private Map<String, PageElements> pageTemplateCache = new HashMap<String, PageElements>();
	
	@Resource
	private Routing routing;
	@Resource
	private OgnlExUtil ognlExUtil;
	@Resource
	private SafeManager safeManager;
	@Resource
	private BeanIntrospector beanIntrospector;
	@Resource
	private Functions functions;
	
	////////////////////////////////PageConverter ////////////////////////////
	@Resource
	private ListPageConverter listPageConverter;
	@Resource
	private ModelPageConverter modelPageConverter;
	@Resource
	private DateElementConverter dateElementConverter;
	@Resource
	private FileElementConverter fileElementConverter;
	@Resource
	private StringElementConverter stringElementConverter;
	@Resource
	private NumberElementConverter numberElementConverter;
	@Resource
	private BooleanElementConverter booleanElementConverter;
	@Resource
	private PageInfoPageConverter pageInfoPageConverter;
	@Resource
	private TextFieldElementConverter defaultElementConverter;
	
	@PostConstruct
	public void registerDefault() {		
		registerConverter(List.class.getName(), listPageConverter);
		// TODO: disable object conversion, it's too flexible
		//registerConverter(Object.class.getName(), modelPageConverter);
		registerConverter(Date.class.getName(), dateElementConverter);
		registerConverter(File.class.getName(), fileElementConverter);
		registerConverter(String.class.getName(), stringElementConverter);
		registerConverter(Number.class.getName(), numberElementConverter);
		registerConverter(Boolean.class.getName(), booleanElementConverter);
		registerConverter(PageInfo.class.getName(), pageInfoPageConverter);
	}
	
	/**
	 * Cache management
	 * 
	 * @param actionOrModelName
	 */
	public void reset(String actionOrModelName) {
		pageTemplateCache.remove(actionOrModelName);
	}
	
	/**
	 * Cache management
	 * 
	 * @param actionOrModelName
	 */
	public void resetAll(String actionOrModelName) {
		String prefix = actionOrModelName;
		Iterator<String> iter = pageTemplateCache.keySet().iterator();
		while (iter.hasNext()) {
			String cacheKey = iter.next();
			if (cacheKey.startsWith(prefix)) {
				iter.remove();
			}
		}
	}
	
	/**
	 * Register converter to the class
	 * 
	 * @param className
	 * @param elementConverter
	 */
	public void registerConverter(String className, ElementConverter elementConverter) {
		classConverters.put(className, elementConverter);
	}
	
	public Map<String, ElementConverter> getClassConverters() {
		return classConverters;
	}

	public PageElements convertActionClass(Class<?> actionClass, String template) {
		Invoker invoker = routing.action2Invoker(actionClass.getName());
		PageElements pageElements = convertObject(null, invoker, template);
		return pageElements;
	}
	
	public PageElements convertClass(String className, String method, String template) {
		return convertObject(null, new Invoker(className, '.', method), template);
	}
	
	public PageElements convertObject(Object instance, String method) {
		Invoker invoker = new Invoker(instance.getClass().getName(), '.', method);
		return convertObject(instance, invoker, null);
	}
	
	// this method is called by the framework, template is passed through parameter '_tpl'
	public PageElements convertObject(Object instance, Invoker invoker, String template) {
		String method = invoker.methodName;
		if (method == null) {
			return new PageElements(ognlExUtil);
		}
		
		String actionName = invoker.actionName;
		// Each method has a page template
		String key = actionName + template;
		PageElements pageElements = null;
		
		// Try to load it from the user cache
		pageElements = (PageElements) ServletActionContext.getRequest().getSession().getAttribute(key);
		if (pageElements == null) {
			
			// Try to load page structure from the cache
			pageElements = pageTemplateCache.get(key);
	
			if (pageElements == null) {
				// try class as model property, but the property name is null
				PropertyInfo propertyInfo = new PropertyInfo(actionName, null);
				// the define class is itself
				ConversionContext context = new ConversionContext(actionName, instance, 
						propertyInfo, invoker,	0, template, new HashMap<String, PageElements>());
				
				// now, begin to convert
				PageElement element = convert(context);
				
				// update the request url & method
				if (element instanceof PageElements) {
					pageElements = (PageElements) element;
				} else {
					pageElements = new PageElements(ognlExUtil);
					if (element != null) {
						pageElements.addElement(element);
					}
				}
				pageElements.setAction(routing.invoker2Path(invoker));
				pageElements.setMethod(invoker.methodName);
				if (invoker.isInputMethod()) {
					Invoker cloned = invoker.clone();
					cloned.methodName = invoker.getSubmitMethod();
					pageElements.setTargetAction(routing.invoker2Path(cloned));
				} else {
					pageElements.setTargetAction(pageElements.getAction());
				}
				
				// enable the cache
				pageTemplateCache.put(key, pageElements);
				pageElements.refreshCache();
			}
			
			PageElements cloned = pageElements.clone();
			cloned.refreshCache();
			pageElements = cloned;
			pageElements.setValue(instance);
			filterReadPermission(invoker, pageElements);
			// cache the page elements which is filtered by permission framework
			ServletActionContext.getRequest().getSession().setAttribute(key, pageElements);
		}
		

		// For the cached model, we need create a clone instance for the page reference
		// Clone a new instance according to the existed one
		PageElements cloned = pageElements.clone();
		cloned.refreshCache();
		pageElements = cloned;
		pageElements.setValue(instance);
		
		// show the page element structure (only show the first children for the performance reason)
		if (logger.isDebugEnabled()) {
			StringBuilder sb = new StringBuilder();
			sb.append("Cache page structure for invoker: ").append(invoker).append('\n');
			sb.append("====action: ").append(pageElements.getAction()).append("=====\n");
			Map<String, PageElement> elements = pageElements.getAllElements();
			
			for (PageElement theElement : elements.values()) {
				sb.append(theElement).append('\n');
			}
			sb.append("=================================================");
			logger.debug(sb.toString());
		}
		return pageElements;
	}

	private void filterReadPermission(Invoker invoker, PageElements elements) {
		UserInfo userInfo = safeManager.getLoginUser(ServletActionContext.getRequest());
		if (userInfo == null) {
			return ;
		}
		
		String userId = userInfo.getLoginId();
		String modelName = beanIntrospector.getModelName(invoker.actionName);
		class FieldName {
			String prefix;
			Set<String> fields;
			
			public FieldName(String prefix, Set<String> fields) {
				this.prefix = prefix;
				this.fields = fields;
			}
		}
		Map<String, FieldName> modelFieldMapping = new HashMap<String, FieldName>();
		Map<String, PageElement> elementMapping = new HashMap<String, PageElement>();
		
		// scan all page element
		for (PageElement element : elements.getAllElements().values()) {
			String name = element.getName();
			if (name == null || name.charAt(0) == '_') {
				continue;
			}
			
			int index = name.length();
			int end = index;
			// e.g name = 'model.elecEventItem.partNumber', modelName='com.espirit.sale.rfqItem.SearchAction'
			while ((index = name.lastIndexOf('.', index - 1)) > 0) {
				String prefix = name.substring(0, index);
				String parentModelName = beanIntrospector.getDescriptor(modelName, prefix);
				if (parentModelName == null) {
					continue;
				}
				
				String fieldName = name.substring(index + 1, end);
				end = index;
				FieldName fields = modelFieldMapping.get(parentModelName);
				
				if (fields == null) {
					fields = new FieldName(prefix, new HashSet<String>());
					modelFieldMapping.put(parentModelName, fields);				
				}
				fields.fields.add(fieldName);
				elementMapping.put(parentModelName + '.' + fieldName, element);
			}
		}
		
		// update the field permission
		ValueStack stack = ActionContext.getContext().getValueStack();
		stack.getContext().put(FrameConstant.STACK_PAGEELEMENTS, elements);
    	Context context = new Context(stack);
		for (Entry<String, FieldName> entry : modelFieldMapping.entrySet()) {
			String[] fieldNames = new String[entry.getValue().fields.size()];
			entry.getValue().fields.toArray(fieldNames);
			
			String modelPath = functions.linkModelNoBase(entry.getKey(), invoker.methodName);	// invoker.methodName
			safeManager.checkReadField(userId, modelPath, context, true, entry.getValue().prefix, fieldNames);
			safeManager.checkWriteField(userId, modelPath, context, true, entry.getValue().prefix, fieldNames);
			
			List<Class> subClasses = beanIntrospector.getSubClass(entry.getKey());
			if (subClasses != null && subClasses.size() > 0) {
				for (Class subClass : subClasses) {
					modelPath = functions.linkModelNoBase(subClass.getName(), invoker.methodName);
					// check the runtime permission
					String foundFieldReadPermission = safeManager.checkReadField(userId, modelPath, context, false, null, fieldNames);
					if (foundFieldReadPermission != null) {
						for (String fieldName : fieldNames) {
							elementMapping.get(entry.getKey() + '.' + fieldName).addExtraParams(
									FrameConstant.SEC_CHECK_FIELD, "R");
						}
					}
					
					// check the runtime permission
					boolean foundFieldWritePermission = !safeManager.checkWriteField(userId, modelPath, context, false, null, fieldNames);
					if (foundFieldWritePermission) {
						for (String fieldName : fieldNames) {
							elementMapping.get(entry.getKey() + '.' + fieldName).addExtraParams(
									FrameConstant.SEC_CHECK_FIELD, "W");
						}
					}
				}// for
			}
		}// for
	}
	
	/**
	 * The core method, it convert the object to PageElement
	 * 
	 * @param context
	 * @return
	 */
	public PageElement convert(ConversionContext context) {
		PropertyInfo propertyInfo = context.getPropertyInfo();
		
		if (context.getPropertyValue() != null) {
			propertyInfo = reviseGenericPropertyTypeByInstance(propertyInfo, context.getPropertyValue());
			context.setPropertyInfo(propertyInfo);
		}
		
		String className = propertyInfo.getType();
		boolean isArray = ClassUtil.isCollection(propertyInfo.getTypeClass());
		String genericType = null;
		if (!isArray) {	// array is special, we only convert the original type
			genericType = propertyInfo.getGenericType();
		}
		
		// Find the matched converter
		ElementConverter converter = null;
		for (String name : getHierarchy(className, genericType)) {
			converter = classConverters.get(name);
			if (converter != null) {
				break;
			}
		}
		
		context.setLevel(context.getLevel() + 1);
		PageElement element = null;
		if (converter != null) {
			element = converter.convert(context);
		}
		if (element == null) {
			defaultElementConverter.convert(context);
		}
		return element;
	}
	
	protected List<String> getHierarchy(String className, String genericType) {
		List<String> hierarchy = new ArrayList<String>();
		if (genericType != null && !genericType.equals(className)) {
			hierarchy.add(genericType);
		}
		hierarchy.add(className);
		
		try {
			Class clazz = ClassUtils.getClass(className);
			if (clazz.isPrimitive()) {
				clazz = getWrapperClass(clazz);
				hierarchy.add(clazz.getName());
			}
			
			for (Class<?> intf : clazz.getInterfaces()) {
				hierarchy.add(intf.getName());
			}
			Class superClazz = clazz.getSuperclass();
			while (superClazz != null) {
				hierarchy.add(superClazz.getName());
				superClazz = superClazz.getSuperclass();
			}
		} catch (Exception ex) {
			logger.debug(className + " is not a static class");
		}
		// all class is an instance of Object.class
		hierarchy.add(Object.class.getName());
		return hierarchy;
	}
	
	private Class getWrapperClass(Class clazz) {
		return ClassUtils.primitiveToWrapper(clazz);
	}

	private PropertyInfo reviseGenericPropertyTypeByInstance(PropertyInfo propertyInfo, Object instance) {
		Class<?> clazz = instance.getClass();
		String genericType = null;
		if (ClassUtil.isCollection(clazz)) {
			Collection coll = (Collection) instance;
			if (coll.size() > 0) {
				genericType = coll.iterator().next().getClass().getName();
			}
		} else if (instance instanceof DynamicModel) {
			DynamicModel dynamicModel = (DynamicModel)instance;
			genericType = dynamicModel.getEntityName();
		} else {
			genericType = instance.getClass().getName();
		}
		if (!propertyInfo.getGenericType().equals(genericType)) {
			PropertyInfo realInfo = propertyInfo.clone();
			realInfo.setGenericType(genericType);
			Class<?> type = realInfo.getTypeClass();
			realInfo.setSimpleObj(ClassUtil.isSimpleObj(type));
			
			return realInfo;
		} else {
			return propertyInfo;
		}
	}
}